From ea3f83754f83d23dcba821ec4da9fb22cdea2bfe Mon Sep 17 00:00:00 2001 From: Tomasz Grabiec Date: Fri, 23 Jan 2015 20:20:34 +0100 Subject: [PATCH] service: convert ClientState --- service/ClientState.java | 325 --------------------------------------- service/client_state.hh | 314 ++++++++++++++++++++++++++++++++++++- unimplemented.hh | 5 + 3 files changed, 311 insertions(+), 333 deletions(-) delete mode 100644 service/ClientState.java diff --git a/service/ClientState.java b/service/ClientState.java deleted file mode 100644 index dcdf8386a2..0000000000 --- a/service/ClientState.java +++ /dev/null @@ -1,325 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.cassandra.service; - -import java.net.SocketAddress; -import java.util.*; -import java.util.concurrent.ExecutionException; -import java.util.concurrent.atomic.AtomicLong; - -import com.google.common.collect.Iterables; -import com.google.common.collect.Sets; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import org.apache.cassandra.auth.*; -import org.apache.cassandra.config.DatabaseDescriptor; -import org.apache.cassandra.config.Schema; -import org.apache.cassandra.cql3.QueryHandler; -import org.apache.cassandra.cql3.QueryProcessor; -import org.apache.cassandra.schema.LegacySchemaTables; -import org.apache.cassandra.db.SystemKeyspace; -import org.apache.cassandra.exceptions.AuthenticationException; -import org.apache.cassandra.exceptions.InvalidRequestException; -import org.apache.cassandra.exceptions.UnauthorizedException; -import org.apache.cassandra.tracing.TraceKeyspace; -import org.apache.cassandra.thrift.ThriftValidation; -import org.apache.cassandra.utils.FBUtilities; -import org.apache.cassandra.utils.JVMStabilityInspector; -import org.apache.cassandra.utils.Pair; -import org.apache.cassandra.utils.SemanticVersion; - -/** - * State related to a client connection. - */ -public class ClientState -{ - private static final Logger logger = LoggerFactory.getLogger(ClientState.class); - public static final SemanticVersion DEFAULT_CQL_VERSION = org.apache.cassandra.cql3.QueryProcessor.CQL_VERSION; - - private static final Set READABLE_SYSTEM_RESOURCES = new HashSet<>(); - private static final Set PROTECTED_AUTH_RESOURCES = new HashSet<>(); - - static - { - // We want these system cfs to be always readable to authenticated users since many tools rely on them - // (nodetool, cqlsh, bulkloader, etc.) - for (String cf : Iterables.concat(Arrays.asList(SystemKeyspace.LOCAL, SystemKeyspace.PEERS), LegacySchemaTables.ALL)) - READABLE_SYSTEM_RESOURCES.add(DataResource.columnFamily(SystemKeyspace.NAME, cf)); - - PROTECTED_AUTH_RESOURCES.addAll(DatabaseDescriptor.getAuthenticator().protectedResources()); - PROTECTED_AUTH_RESOURCES.addAll(DatabaseDescriptor.getAuthorizer().protectedResources()); - } - - // Current user for the session - private volatile AuthenticatedUser user; - private volatile String keyspace; - - private static final QueryHandler cqlQueryHandler; - static - { - QueryHandler handler = QueryProcessor.instance; - String customHandlerClass = System.getProperty("cassandra.custom_query_handler_class"); - if (customHandlerClass != null) - { - try - { - handler = (QueryHandler)FBUtilities.construct(customHandlerClass, "QueryHandler"); - logger.info("Using {} as query handler for native protocol queries (as requested with -Dcassandra.custom_query_handler_class)", customHandlerClass); - } - catch (Exception e) - { - JVMStabilityInspector.inspectThrowable(e); - logger.info("Cannot use class {} as query handler ({}), ignoring by defaulting on normal query handling", customHandlerClass, e.getMessage()); - } - } - cqlQueryHandler = handler; - } - - // isInternal is used to mark ClientState as used by some internal component - // that should have an ability to modify system keyspace. - public final boolean isInternal; - - // The remote address of the client - null for internal clients. - private final SocketAddress remoteAddress; - - // The biggest timestamp that was returned by getTimestamp/assigned to a query - private final AtomicLong lastTimestampMicros = new AtomicLong(0); - - /** - * Construct a new, empty ClientState for internal calls. - */ - private ClientState() - { - this.isInternal = true; - this.remoteAddress = null; - } - - protected ClientState(SocketAddress remoteAddress) - { - this.isInternal = false; - this.remoteAddress = remoteAddress; - if (!DatabaseDescriptor.getAuthenticator().requireAuthentication()) - this.user = AuthenticatedUser.ANONYMOUS_USER; - } - - /** - * @return a ClientState object for internal C* calls (not limited by any kind of auth). - */ - public static ClientState forInternalCalls() - { - return new ClientState(); - } - - /** - * @return a ClientState object for external clients (thrift/native protocol users). - */ - public static ClientState forExternalCalls(SocketAddress remoteAddress) - { - return new ClientState(remoteAddress); - } - - /** - * This clock guarantees that updates for the same ClientState will be ordered - * in the sequence seen, even if multiple updates happen in the same millisecond. - */ - public long getTimestamp() - { - while (true) - { - long current = System.currentTimeMillis() * 1000; - long last = lastTimestampMicros.get(); - long tstamp = last >= current ? last + 1 : current; - if (lastTimestampMicros.compareAndSet(last, tstamp)) - return tstamp; - } - } - - /** - * Can be use when a timestamp has been assigned by a query, but that timestamp is - * not directly one returned by getTimestamp() (see SP.beginAndRepairPaxos()). - * This ensure following calls to getTimestamp() will return a timestamp strictly - * greated than the one provided to this method. - */ - public void updateLastTimestamp(long tstampMicros) - { - while (true) - { - long last = lastTimestampMicros.get(); - if (tstampMicros <= last || lastTimestampMicros.compareAndSet(last, tstampMicros)) - return; - } - } - - public static QueryHandler getCQLQueryHandler() - { - return cqlQueryHandler; - } - - public SocketAddress getRemoteAddress() - { - return remoteAddress; - } - - public String getRawKeyspace() - { - return keyspace; - } - - public String getKeyspace() throws InvalidRequestException - { - if (keyspace == null) - throw new InvalidRequestException("No keyspace has been specified. USE a keyspace, or explicitly specify keyspace.tablename"); - return keyspace; - } - - public void setKeyspace(String ks) throws InvalidRequestException - { - // Skip keyspace validation for non-authenticated users. Apparently, some client libraries - // call set_keyspace() before calling login(), and we have to handle that. - if (user != null && Schema.instance.getKSMetaData(ks) == null) - throw new InvalidRequestException("Keyspace '" + ks + "' does not exist"); - keyspace = ks; - } - - /** - * Attempts to login the given user. - */ - public void login(AuthenticatedUser user) throws AuthenticationException - { - if (!user.isAnonymous() && !Auth.isExistingUser(user.getName())) - throw new AuthenticationException(String.format("User %s doesn't exist - create it with CREATE USER query first", - user.getName())); - this.user = user; - } - - public void hasAllKeyspacesAccess(Permission perm) throws UnauthorizedException - { - if (isInternal) - return; - validateLogin(); - ensureHasPermission(perm, DataResource.root()); - } - - public void hasKeyspaceAccess(String keyspace, Permission perm) throws UnauthorizedException, InvalidRequestException - { - hasAccess(keyspace, perm, DataResource.keyspace(keyspace)); - } - - public void hasColumnFamilyAccess(String keyspace, String columnFamily, Permission perm) - throws UnauthorizedException, InvalidRequestException - { - ThriftValidation.validateColumnFamily(keyspace, columnFamily); - hasAccess(keyspace, perm, DataResource.columnFamily(keyspace, columnFamily)); - } - - private void hasAccess(String keyspace, Permission perm, DataResource resource) - throws UnauthorizedException, InvalidRequestException - { - validateKeyspace(keyspace); - if (isInternal) - return; - validateLogin(); - preventSystemKSSchemaModification(keyspace, resource, perm); - if ((perm == Permission.SELECT) && READABLE_SYSTEM_RESOURCES.contains(resource)) - return; - if (PROTECTED_AUTH_RESOURCES.contains(resource)) - if ((perm == Permission.CREATE) || (perm == Permission.ALTER) || (perm == Permission.DROP)) - throw new UnauthorizedException(String.format("%s schema is protected", resource)); - ensureHasPermission(perm, resource); - } - - public void ensureHasPermission(Permission perm, IResource resource) throws UnauthorizedException - { - for (IResource r : Resources.chain(resource)) - if (authorize(r).contains(perm)) - return; - - throw new UnauthorizedException(String.format("User %s has no %s permission on %s or any of its parents", - user.getName(), - perm, - resource)); - } - - private void preventSystemKSSchemaModification(String keyspace, DataResource resource, Permission perm) throws UnauthorizedException - { - // we only care about schema modification. - if (!((perm == Permission.ALTER) || (perm == Permission.DROP) || (perm == Permission.CREATE))) - return; - - // prevent system keyspace modification - if (SystemKeyspace.NAME.equalsIgnoreCase(keyspace)) - throw new UnauthorizedException(keyspace + " keyspace is not user-modifiable."); - - // we want to allow altering AUTH_KS and TRACING_KS. - Set allowAlter = Sets.newHashSet(Auth.AUTH_KS, TraceKeyspace.NAME); - if (allowAlter.contains(keyspace.toLowerCase()) && !(resource.isKeyspaceLevel() && (perm == Permission.ALTER))) - throw new UnauthorizedException(String.format("Cannot %s %s", perm, resource)); - } - - public void validateLogin() throws UnauthorizedException - { - if (user == null) - throw new UnauthorizedException("You have not logged in"); - } - - public void ensureNotAnonymous() throws UnauthorizedException - { - validateLogin(); - if (user.isAnonymous()) - throw new UnauthorizedException("You have to be logged in and not anonymous to perform this request"); - } - - public void ensureIsSuper(String message) throws UnauthorizedException - { - if (DatabaseDescriptor.getAuthenticator().requireAuthentication() && (user == null || !user.isSuper())) - throw new UnauthorizedException(message); - } - - private static void validateKeyspace(String keyspace) throws InvalidRequestException - { - if (keyspace == null) - throw new InvalidRequestException("You have not set a keyspace for this session"); - } - - public AuthenticatedUser getUser() - { - return user; - } - - public static SemanticVersion[] getCQLSupportedVersion() - { - return new SemanticVersion[]{ QueryProcessor.CQL_VERSION }; - } - - private Set authorize(IResource resource) - { - // AllowAllAuthorizer or manually disabled caching. - if (Auth.permissionsCache == null) - return DatabaseDescriptor.getAuthorizer().authorize(user, resource); - - try - { - return Auth.permissionsCache.get(Pair.create(user, resource)); - } - catch (ExecutionException e) - { - throw new RuntimeException(e); - } - } -} diff --git a/service/client_state.hh b/service/client_state.hh index eb78cfbd92..c1f3583287 100644 --- a/service/client_state.hh +++ b/service/client_state.hh @@ -1,27 +1,325 @@ -#ifndef SERVICE_CLIENT_STATE_HH -#define SERVICE_CLIENT_STATE_HH +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* + * Copyright 2015 Cloudius Systems + * + * Modified by Cloudius Systems + */ + +#pragma once + +#include "exceptions/exceptions.hh" +#include "unimplemented.hh" namespace service { +/** + * State related to a client connection. + */ class client_state { private: sstring _keyspace; +#if 0 + private static final Logger logger = LoggerFactory.getLogger(ClientState.class); + public static final SemanticVersion DEFAULT_CQL_VERSION = org.apache.cassandra.cql3.QueryProcessor.CQL_VERSION; -public: - // FIXME: stub + private static final Set READABLE_SYSTEM_RESOURCES = new HashSet<>(); + private static final Set PROTECTED_AUTH_RESOURCES = new HashSet<>(); - void validate_login() const { + static + { + // We want these system cfs to be always readable to authenticated users since many tools rely on them + // (nodetool, cqlsh, bulkloader, etc.) + for (String cf : Iterables.concat(Arrays.asList(SystemKeyspace.LOCAL, SystemKeyspace.PEERS), LegacySchemaTables.ALL)) + READABLE_SYSTEM_RESOURCES.add(DataResource.columnFamily(SystemKeyspace.NAME, cf)); + + PROTECTED_AUTH_RESOURCES.addAll(DatabaseDescriptor.getAuthenticator().protectedResources()); + PROTECTED_AUTH_RESOURCES.addAll(DatabaseDescriptor.getAuthorizer().protectedResources()); } + // Current user for the session + private volatile AuthenticatedUser user; + private volatile String keyspace; + + private static final QueryHandler cqlQueryHandler; + static + { + QueryHandler handler = QueryProcessor.instance; + String customHandlerClass = System.getProperty("cassandra.custom_query_handler_class"); + if (customHandlerClass != null) + { + try + { + handler = (QueryHandler)FBUtilities.construct(customHandlerClass, "QueryHandler"); + logger.info("Using {} as query handler for native protocol queries (as requested with -Dcassandra.custom_query_handler_class)", customHandlerClass); + } + catch (Exception e) + { + JVMStabilityInspector.inspectThrowable(e); + logger.info("Cannot use class {} as query handler ({}), ignoring by defaulting on normal query handling", customHandlerClass, e.getMessage()); + } + } + cqlQueryHandler = handler; + } + + // isInternal is used to mark ClientState as used by some internal component + // that should have an ability to modify system keyspace. + public final boolean isInternal; + + // The remote address of the client - null for internal clients. + private final SocketAddress remoteAddress; + + // The biggest timestamp that was returned by getTimestamp/assigned to a query + private final AtomicLong lastTimestampMicros = new AtomicLong(0); + + /** + * Construct a new, empty ClientState for internal calls. + */ + private ClientState() + { + this.isInternal = true; + this.remoteAddress = null; + } + + protected ClientState(SocketAddress remoteAddress) + { + this.isInternal = false; + this.remoteAddress = remoteAddress; + if (!DatabaseDescriptor.getAuthenticator().requireAuthentication()) + this.user = AuthenticatedUser.ANONYMOUS_USER; + } + + /** + * @return a ClientState object for internal C* calls (not limited by any kind of auth). + */ + public static ClientState forInternalCalls() + { + return new ClientState(); + } + + /** + * @return a ClientState object for external clients (thrift/native protocol users). + */ + public static ClientState forExternalCalls(SocketAddress remoteAddress) + { + return new ClientState(remoteAddress); + } + + /** + * This clock guarantees that updates for the same ClientState will be ordered + * in the sequence seen, even if multiple updates happen in the same millisecond. + */ + public long getTimestamp() + { + while (true) + { + long current = System.currentTimeMillis() * 1000; + long last = lastTimestampMicros.get(); + long tstamp = last >= current ? last + 1 : current; + if (lastTimestampMicros.compareAndSet(last, tstamp)) + return tstamp; + } + } + + /** + * Can be use when a timestamp has been assigned by a query, but that timestamp is + * not directly one returned by getTimestamp() (see SP.beginAndRepairPaxos()). + * This ensure following calls to getTimestamp() will return a timestamp strictly + * greated than the one provided to this method. + */ + public void updateLastTimestamp(long tstampMicros) + { + while (true) + { + long last = lastTimestampMicros.get(); + if (tstampMicros <= last || lastTimestampMicros.compareAndSet(last, tstampMicros)) + return; + } + } + + public static QueryHandler getCQLQueryHandler() + { + return cqlQueryHandler; + } + + public SocketAddress getRemoteAddress() + { + return remoteAddress; + } + + public String getRawKeyspace() + { + return keyspace; + } +#endif + +public: void set_keyspace(sstring keyspace) { +#if 0 + // Skip keyspace validation for non-authenticated users. Apparently, some client libraries + // call set_keyspace() before calling login(), and we have to handle that. + if (user && Schema.instance.getKSMetaData(ks) == null) { + throw exceptions::invalid_request_exception(sprint("Keyspace '%s' does not exist", keyspace); + } +#endif _keyspace = keyspace; } sstring get_keyspace() const { + if (_keyspace.empty()) { + throw exceptions::invalid_request_exception("No keyspace has been specified. USE a keyspace, or explicitly specify keyspace.tablename"); + } return _keyspace; - } + } + +#if 0 + /** + * Attempts to login the given user. + */ + public void login(AuthenticatedUser user) throws AuthenticationException + { + if (!user.isAnonymous() && !Auth.isExistingUser(user.getName())) + throw new AuthenticationException(String.format("User %s doesn't exist - create it with CREATE USER query first", + user.getName())); + this.user = user; + } + + public void hasAllKeyspacesAccess(Permission perm) throws UnauthorizedException + { + if (isInternal) + return; + validateLogin(); + ensureHasPermission(perm, DataResource.root()); + } + + public void hasKeyspaceAccess(String keyspace, Permission perm) throws UnauthorizedException, InvalidRequestException + { + hasAccess(keyspace, perm, DataResource.keyspace(keyspace)); + } + + public void hasColumnFamilyAccess(String keyspace, String columnFamily, Permission perm) + throws UnauthorizedException, InvalidRequestException + { + ThriftValidation.validateColumnFamily(keyspace, columnFamily); + hasAccess(keyspace, perm, DataResource.columnFamily(keyspace, columnFamily)); + } + + private void hasAccess(String keyspace, Permission perm, DataResource resource) + throws UnauthorizedException, InvalidRequestException + { + validateKeyspace(keyspace); + if (isInternal) + return; + validateLogin(); + preventSystemKSSchemaModification(keyspace, resource, perm); + if ((perm == Permission.SELECT) && READABLE_SYSTEM_RESOURCES.contains(resource)) + return; + if (PROTECTED_AUTH_RESOURCES.contains(resource)) + if ((perm == Permission.CREATE) || (perm == Permission.ALTER) || (perm == Permission.DROP)) + throw new UnauthorizedException(String.format("%s schema is protected", resource)); + ensureHasPermission(perm, resource); + } + + public void ensureHasPermission(Permission perm, IResource resource) throws UnauthorizedException + { + for (IResource r : Resources.chain(resource)) + if (authorize(r).contains(perm)) + return; + + throw new UnauthorizedException(String.format("User %s has no %s permission on %s or any of its parents", + user.getName(), + perm, + resource)); + } + + private void preventSystemKSSchemaModification(String keyspace, DataResource resource, Permission perm) throws UnauthorizedException + { + // we only care about schema modification. + if (!((perm == Permission.ALTER) || (perm == Permission.DROP) || (perm == Permission.CREATE))) + return; + + // prevent system keyspace modification + if (SystemKeyspace.NAME.equalsIgnoreCase(keyspace)) + throw new UnauthorizedException(keyspace + " keyspace is not user-modifiable."); + + // we want to allow altering AUTH_KS and TRACING_KS. + Set allowAlter = Sets.newHashSet(Auth.AUTH_KS, TraceKeyspace.NAME); + if (allowAlter.contains(keyspace.toLowerCase()) && !(resource.isKeyspaceLevel() && (perm == Permission.ALTER))) + throw new UnauthorizedException(String.format("Cannot %s %s", perm, resource)); + } +#endif + +public: + void validate_login() const { + unimplemented::auth(); +#if 0 + if (user == null) + throw new UnauthorizedException("You have not logged in"); +#endif + } + +#if 0 + public void ensureNotAnonymous() throws UnauthorizedException + { + validateLogin(); + if (user.isAnonymous()) + throw new UnauthorizedException("You have to be logged in and not anonymous to perform this request"); + } + + public void ensureIsSuper(String message) throws UnauthorizedException + { + if (DatabaseDescriptor.getAuthenticator().requireAuthentication() && (user == null || !user.isSuper())) + throw new UnauthorizedException(message); + } + + private static void validateKeyspace(String keyspace) throws InvalidRequestException + { + if (keyspace == null) + throw new InvalidRequestException("You have not set a keyspace for this session"); + } + + public AuthenticatedUser getUser() + { + return user; + } + + public static SemanticVersion[] getCQLSupportedVersion() + { + return new SemanticVersion[]{ QueryProcessor.CQL_VERSION }; + } + + private Set authorize(IResource resource) + { + // AllowAllAuthorizer or manually disabled caching. + if (Auth.permissionsCache == null) + return DatabaseDescriptor.getAuthorizer().authorize(user, resource); + + try + { + return Auth.permissionsCache.get(Pair.create(user, resource)); + } + catch (ExecutionException e) + { + throw new RuntimeException(e); + } + } +#endif }; } - -#endif diff --git a/unimplemented.hh b/unimplemented.hh index 7084973f99..d6d33ec28a 100644 --- a/unimplemented.hh +++ b/unimplemented.hh @@ -18,4 +18,9 @@ void indexes() { warn("indexes"); } +static inline +void auth() { + warn("auth"); +} + }