creating and viewing grants
10 files added
10 files modified
| | |
| | | |
| | | As you can see, there something special: |
| | | From the 'Role customer#xyz.owner' to the 'Role customer#xyz.admin' there is a dashed line, whereas all other lines are solid lines. |
| | | Solid lines means, that one role is granted to another and followed in all queries to the restricted views. |
| | | The dashed line means that one role is granted to another but not automatically followed in queries to the restricted views. |
| | | Solid lines means, that one role is granted to another and automatically assumed in all queries to the restricted views. |
| | | The dashed line means that one role is granted to another but not automatically assumed in queries to the restricted views. |
| | | |
| | | The reason here is that otherwise simply too many objects would be accessible to those with the 'administrators' role and all queries would be slowed down vastly. |
| | | |
| | | Grants which are not followed are still valid grants for `hsadminng.assumedRoles`. |
| | | Grants which are not automatically assumed are still valid grants for `hsadminng.assumedRoles`. |
| | | Thus, if you want to access anything below a customer, assume its role first. |
| | | |
| | | There is actually another speciality in the customer roles: |
| | |
| | | Both variants a viable option, depending on other needs, e.g. updatable views. |
| | | |
| | | |
| | | ## Access Control to RBAC-Objects |
| | | |
| | | Access Control for business objects checked according to the assigned roles. |
| | | But we decided not to create such roles and permissions for the RBAC-Objects itself. |
| | | It would have overcomplicated the system and the necessary information can easily be added to the RBAC-Objects itself, mostly the `RbacGrant`s. |
| | | |
| | | ### RbacUser |
| | | |
| | | Users can self-register, thus to create a new RbacUser entity, no login is required. |
| | | But such a user has no access-rights except viewing itself. |
| | | |
| | | Users can view themselves. |
| | | And any user can view all other users as long as they have the same roles assigned. |
| | | As an exception, users which are assigned to global roles are not visible by other users. |
| | | |
| | | At least an indirect lookup of known user-names (e.g. email address of the user) is possible |
| | | by users who have an empowered assignment of any role. |
| | | Otherwise, it would not be possible to assign roles to new users. |
| | | |
| | | ### RbacRole |
| | | |
| | | All roles are system-defined and cannot be created or modified by any external API. |
| | | |
| | | Users can view only the roles to which they are assigned. |
| | | |
| | | ## RbacGrant |
| | | |
| | | Grant can be `empowered`, this means that the grantee user can grant the granted role to other users |
| | | and revoke grants to that role. |
| | | (TODO: access control part not yet implemented) |
| | | |
| | | Grants can be `managed`, which means they are created and deleted by system-defined rules. |
| | | If a grant is not managed, it was created by an empowered user and can be deleted by empowered users. |
| | | |
| | | Grants can be `assumed`, which means that they are immediately active. |
| | | If a grant is not assumed, the grantee user needs to use `assumeRoles` to activate it. |
| | | |
| | | Users can see only grants of roles to which they are (directly?) assigned themselves. |
| | | |
| | | TODO: If a user grants an indirect role to another user, that grant would not be visible to the user. |
| | | But if we make indirect grants visible, this would reveal too much information. |
| | | We also cannot keep the granting user in the grant because grants must survive deleted users, |
| | | e.g. if after an account was transferred to another user. |
| | | |
New file |
| | |
| | | package net.hostsharing.hsadminng.rbac.rbacgrant; |
| | | |
| | | import net.hostsharing.hsadminng.context.Context; |
| | | import net.hostsharing.hsadminng.generated.api.v1.api.RbacgrantsApi; |
| | | import net.hostsharing.hsadminng.generated.api.v1.api.RbacrolesApi; |
| | | import net.hostsharing.hsadminng.generated.api.v1.model.RbacGrantResource; |
| | | import net.hostsharing.hsadminng.generated.api.v1.model.RbacRoleResource; |
| | | import org.springframework.beans.factory.annotation.Autowired; |
| | | import org.springframework.http.ResponseEntity; |
| | | import org.springframework.web.bind.annotation.RestController; |
| | | import org.springframework.web.servlet.mvc.method.annotation.MvcUriComponentsBuilder; |
| | | |
| | | import javax.transaction.Transactional; |
| | | import java.util.List; |
| | | |
| | | import static net.hostsharing.hsadminng.Mapper.map; |
| | | import static net.hostsharing.hsadminng.Mapper.mapList; |
| | | |
| | | @RestController |
| | | |
| | | public class RbacGrantController implements RbacgrantsApi { |
| | | |
| | | @Autowired |
| | | private Context context; |
| | | |
| | | @Autowired |
| | | private RbacGrantRepository rbacGrantRepository; |
| | | |
| | | @Override |
| | | @Transactional |
| | | public ResponseEntity<List<RbacGrantResource>> listUserGrants( |
| | | final String currentUser, |
| | | final String assumedRoles) { |
| | | |
| | | context.setCurrentUser(currentUser); |
| | | if (assumedRoles != null && !assumedRoles.isBlank()) { |
| | | context.assumeRoles(assumedRoles); |
| | | } |
| | | return ResponseEntity.ok(mapList(rbacGrantRepository.findAll(), RbacGrantResource.class)); |
| | | } |
| | | |
| | | @Override |
| | | @Transactional |
| | | public ResponseEntity<Void> grantRoleToUser( |
| | | final String currentUser, |
| | | final String assumedRoles, |
| | | final RbacGrantResource body) { |
| | | |
| | | context.setCurrentUser(currentUser); |
| | | if (assumedRoles != null && !assumedRoles.isBlank()) { |
| | | context.assumeRoles(assumedRoles); |
| | | } |
| | | |
| | | rbacGrantRepository.save(map(body, RbacGrantEntity.class)); |
| | | |
| | | final var uri = |
| | | MvcUriComponentsBuilder.fromController(getClass()) |
| | | .path("/api/rbac-grants/{roleUuid}") |
| | | .buildAndExpand(body.getRoleUuid()) |
| | | .toUri(); |
| | | return ResponseEntity.created(uri).build(); |
| | | } |
| | | |
| | | } |
New file |
| | |
| | | package net.hostsharing.hsadminng.rbac.rbacgrant; |
| | | |
| | | import lombok.*; |
| | | import net.hostsharing.hsadminng.rbac.rbacrole.RbacRoleType; |
| | | import org.springframework.data.annotation.Immutable; |
| | | |
| | | import javax.persistence.*; |
| | | import java.util.UUID; |
| | | |
| | | @Entity |
| | | @Table(name = "rbacgrants_rv") |
| | | @IdClass(RbacGrantId.class) |
| | | @Getter |
| | | @Setter |
| | | @Builder |
| | | @ToString |
| | | @Immutable |
| | | @NoArgsConstructor |
| | | @AllArgsConstructor |
| | | public class RbacGrantEntity { |
| | | |
| | | @Column(name = "username", updatable = false, insertable = false) |
| | | private String userName; |
| | | |
| | | @Column(name = "roleidname", updatable = false, insertable = false) |
| | | private String roleIdName; |
| | | |
| | | private boolean managed; |
| | | private boolean assumed; |
| | | private boolean empowered; |
| | | |
| | | @Id |
| | | @Column(name = "useruuid") |
| | | private UUID userUuid; |
| | | |
| | | @Id |
| | | @Column(name = "roleuuid") |
| | | private UUID roleUuid; |
| | | |
| | | @Column(name = "objecttable", updatable = false, insertable = false) |
| | | private String objectTable; |
| | | |
| | | @Column(name = "objectuuid", updatable = false, insertable = false) |
| | | private UUID objectUuid; |
| | | |
| | | @Column(name = "objectidname", updatable = false, insertable = false) |
| | | private String objectIdName; |
| | | |
| | | @Column(name = "roletype", updatable = false, insertable = false) |
| | | @Enumerated(EnumType.STRING) |
| | | private RbacRoleType roleType; |
| | | |
| | | public String toDisplay() { |
| | | return "grant( " + userName + " -> " + roleIdName + ": " + |
| | | (managed ? "managed " : "") + |
| | | (assumed ? "assumed " : "") + |
| | | (empowered ? "empowered " : "") + |
| | | ")"; |
| | | } |
| | | } |
New file |
| | |
| | | package net.hostsharing.hsadminng.rbac.rbacgrant; |
| | | |
| | | import lombok.EqualsAndHashCode; |
| | | import lombok.Getter; |
| | | import lombok.NoArgsConstructor; |
| | | |
| | | import java.io.Serializable; |
| | | import java.util.UUID; |
| | | |
| | | @Getter |
| | | @EqualsAndHashCode |
| | | @NoArgsConstructor |
| | | public class RbacGrantId implements Serializable { |
| | | |
| | | private UUID userUuid; |
| | | private UUID roleUuid; |
| | | } |
New file |
| | |
| | | package net.hostsharing.hsadminng.rbac.rbacgrant; |
| | | |
| | | import org.springframework.data.repository.Repository; |
| | | |
| | | import java.util.List; |
| | | |
| | | public interface RbacGrantRepository extends Repository<RbacGrantEntity, RbacGrantId> { |
| | | |
| | | List<RbacGrantEntity> findAll(); |
| | | |
| | | void save(final RbacGrantEntity grant); |
| | | |
| | | } |
| | |
| | | |
| | | @Override |
| | | @Transactional |
| | | public ResponseEntity<List<RbacRoleResource>> listRoles(final String currentUser, final String assumedRoles) { |
| | | public ResponseEntity<List<RbacRoleResource>> listRoles( |
| | | final String currentUser, |
| | | final String assumedRoles) { |
| | | |
| | | context.setCurrentUser(currentUser); |
| | | if (assumedRoles != null && !assumedRoles.isBlank()) { |
| | | context.assumeRoles(assumedRoles); |
| | | } |
| | | return ResponseEntity.ok(mapList(rbacRoleRepository.findAll(), RbacRoleResource.class)); |
| | | } |
| | | |
| | | } |
| | |
| | | """) |
| | | List<RbacUserEntity> findByOptionalNameLike(String userName); |
| | | |
| | | @Query(value = "select uuid from rbacuser where name=:userName", nativeQuery = true) |
| | | UUID findUuidByName(String userName); |
| | | |
| | | RbacUserEntity findByUuid(UUID uuid); |
| | | |
| | | @Query(value = "select * from grantedPermissions(:userName)", nativeQuery = true) |
New file |
| | |
| | | |
| | | components: |
| | | |
| | | schemas: |
| | | |
| | | RbacGrant: |
| | | type: object |
| | | properties: |
| | | userUuid: |
| | | type: string |
| | | format: uuid |
| | | roleUuid: |
| | | type: string |
| | | format: uuid |
| | | assumed: |
| | | type: boolean |
| | | empowered: |
| | | type: boolean |
| | | required: |
| | | - userUuid |
| | | - roleUuid |
New file |
| | |
| | | get: |
| | | tags: |
| | | - rbacgrants |
| | | operationId: listUserGrants |
| | | parameters: |
| | | - $ref: './api-definition/auth.yaml#/components/parameters/currentUser' |
| | | - $ref: './api-definition/auth.yaml#/components/parameters/assumedRoles' |
| | | responses: |
| | | "200": |
| | | description: OK |
| | | content: |
| | | 'application/json': |
| | | schema: |
| | | type: array |
| | | items: |
| | | $ref: './api-definition/rbac-grant-schemas.yaml#/components/schemas/RbacGrant' |
| | | |
| | | post: |
| | | tags: |
| | | - rbacgrants |
| | | operationId: grantRoleToUser |
| | | parameters: |
| | | - $ref: './api-definition/auth.yaml#/components/parameters/currentUser' |
| | | - $ref: './api-definition/auth.yaml#/components/parameters/assumedRoles' |
| | | requestBody: |
| | | required: true |
| | | content: |
| | | application/json: |
| | | schema: |
| | | $ref: './api-definition/rbac-grant-schemas.yaml#/components/schemas/RbacGrant' |
| | | responses: |
| | | "201": |
| | | description: OK |
| | | "401": |
| | | $ref: './api-definition/error-responses.yaml#/components/responses/Unauthorized' |
| | | "403": |
| | | $ref: './api-definition/error-responses.yaml#/components/responses/Forbidden' |
| | | "409": |
| | | $ref: './api-definition/error-responses.yaml#/components/responses/Conflict' |
| | |
| | | begin |
| | | insert |
| | | into RbacReference as r (uuid, type) |
| | | values ( coalesce(refUuid, uuid_generate_v4()), 'RbacUser') |
| | | values (coalesce(refUuid, uuid_generate_v4()), 'RbacUser') |
| | | returning r.uuid into refUuid; |
| | | insert |
| | | into RbacUser (uuid, name) |
| | |
| | | end; |
| | | $$; |
| | | |
| | | create or replace function findRoleId(roleIdName varchar) |
| | | returns uuid |
| | | returns null on null input |
| | | language plpgsql as $$ |
| | | declare |
| | | roleParts text; |
| | | roleTypeFromRoleIdName RbacRoleType; |
| | | objectNameFromRoleIdName text; |
| | | objectTableFromRoleIdName text; |
| | | objectUuidOfRole uuid; |
| | | roleUuid uuid; |
| | | begin |
| | | -- TODO: extract function toRbacRoleDescriptor(roleIdName varchar) + find other occurrences |
| | | roleParts = overlay(roleIdName placing '#' from length(roleIdName) + 1 - strpos(reverse(roleIdName), '.')); |
| | | objectTableFromRoleIdName = split_part(roleParts, '#', 1); |
| | | objectNameFromRoleIdName = split_part(roleParts, '#', 2); |
| | | roleTypeFromRoleIdName = split_part(roleParts, '#', 3); |
| | | objectUuidOfRole = findObjectUuidByIdName(objectTableFromRoleIdName, objectNameFromRoleIdName); |
| | | |
| | | select uuid |
| | | from RbacRole |
| | | where objectUuid = objectUuidOfRole |
| | | and roleType = roleTypeFromRoleIdName |
| | | into roleUuid; |
| | | return roleUuid; |
| | | end; $$; |
| | | |
| | | create or replace function findRoleId(roleDescriptor RbacRoleDescriptor) |
| | | returns uuid |
| | | returns null on null input |
| | |
| | | --changeset rbac-base-GRANTS:1 endDelimiter:--// |
| | | -- ---------------------------------------------------------------------------- |
| | | /* |
| | | |
| | | Table to store grants / role- or permission assignments to users or roles. |
| | | */ |
| | | create table RbacGrants |
| | | ( |
| | | ascendantUuid uuid references RbacReference (uuid) on delete cascade, |
| | | descendantUuid uuid references RbacReference (uuid) on delete cascade, |
| | | follow boolean not null default true, |
| | | managed boolean not null default false, -- created by system (true) vs. user (false) |
| | | assumed boolean not null default true, -- auto assumed (true) vs. needs assumeRoles (false) |
| | | empowered boolean not null default false, -- true: allows grant+revoke for descendant role |
| | | primary key (ascendantUuid, descendantUuid) |
| | | ); |
| | | create index on RbacGrants (ascendantUuid); |
| | |
| | | granteeId uuid; |
| | | begin |
| | | -- TODO: needs optimization |
| | | foreach granteeId in array granteeIds loop |
| | | foreach granteeId in array granteeIds |
| | | loop |
| | | if isGranted(granteeId, grantedId) then |
| | | return true; |
| | | end if; |
| | |
| | | language sql as $$ |
| | | select exists( |
| | | select r.uuid |
| | | from RbacGrants as g |
| | | join RbacRole as r on r.uuid = g.descendantuuid |
| | | join RbacObject as o on o.uuid = r.objectuuid |
| | | where g.ascendantuuid = userUuid and o.objecttable = 'global' |
| | | from RbacGrants as g |
| | | join RbacRole as r on r.uuid = g.descendantuuid |
| | | join RbacObject as o on o.uuid = r.objectuuid |
| | | where g.ascendantuuid = userUuid |
| | | and o.objecttable = 'global' |
| | | ); |
| | | $$; |
| | | |
| | |
| | | perform assertReferenceType('permissionId (descendant)', permissionIds[i], 'RbacPermission'); |
| | | |
| | | insert |
| | | into RbacGrants (ascendantUuid, descendantUuid, follow) |
| | | values (roleUuid, permissionIds[i], true) |
| | | into RbacGrants (ascendantUuid, descendantUuid, managed, assumed, empowered) |
| | | values (roleUuid, permissionIds[i], true, true, false) |
| | | on conflict do nothing; -- allow granting multiple times |
| | | end loop; |
| | | end; |
| | | $$; |
| | | |
| | | create or replace procedure grantRoleToRole(subRoleId uuid, superRoleId uuid, doFollow bool = true) |
| | | create or replace procedure grantRoleToRole(subRoleId uuid, superRoleId uuid, doAssume bool = true) |
| | | language plpgsql as $$ |
| | | begin |
| | | perform assertReferenceType('superRoleId (ascendant)', superRoleId, 'RbacRole'); |
| | |
| | | end if; |
| | | |
| | | insert |
| | | into RbacGrants (ascendantUuid, descendantUuid, follow) |
| | | values (superRoleId, subRoleId, doFollow) |
| | | into RbacGrants (ascendantUuid, descendantUuid, managed, assumed, empowered) |
| | | values (superRoleId, subRoleId, true, doAssume, false) |
| | | on conflict do nothing; -- allow granting multiple times |
| | | end; $$; |
| | | |
| | |
| | | end if; |
| | | end; $$; |
| | | |
| | | create or replace procedure grantRoleToUser(roleId uuid, userId uuid) |
| | | create or replace procedure grantRoleToUser(roleUuid uuid, userUuid uuid) |
| | | language plpgsql as $$ |
| | | begin |
| | | perform assertReferenceType('roleId (ascendant)', roleId, 'RbacRole'); |
| | | perform assertReferenceType('userId (descendant)', userId, 'RbacUser'); |
| | | perform assertReferenceType('roleId (descendant)', roleUuid, 'RbacRole'); |
| | | perform assertReferenceType('userId (ascendant)', userUuid, 'RbacUser'); |
| | | |
| | | insert |
| | | into RbacGrants (ascendantUuid, descendantUuid, follow) |
| | | values (userId, roleId, true) |
| | | on conflict do nothing; -- allow granting multiple times |
| | | into RbacGrants (ascendantUuid, descendantUuid, managed, assumed, empowered) |
| | | values (userUuid, roleUuid, true, true, true); |
| | | -- TODO: What should happen on mupltiple grants? What if options are not the same? |
| | | -- on conflict do nothing; -- allow granting multiple times |
| | | end; $$; |
| | | |
| | | /* |
| | | Attributes of a grant assignment. |
| | | */ |
| | | create type RbacGrantOptions as |
| | | ( |
| | | managed boolean, -- created by system (true) vs. user (false) |
| | | assumed boolean, -- auto assumed (true) vs. needs assumeRoles (false) |
| | | empowered boolean -- true: allows grant+revoke for descendant role |
| | | ); |
| | | |
| | | create or replace procedure grantRoleToUser(roleUuid uuid, userUuid uuid, grantOptions RbacGrantOptions) |
| | | language plpgsql as $$ |
| | | begin |
| | | perform assertReferenceType('roleId (descendant)', roleUuid, 'RbacRole'); |
| | | perform assertReferenceType('userId (ascendant)', userUuid, 'RbacUser'); |
| | | |
| | | if not isGranted(currentSubjectIds(), roleUuid) then |
| | | raise exception '[403] Access to role uuid % forbidden for %', roleUuid, currentSubjects(); |
| | | end if; |
| | | |
| | | insert |
| | | into RbacGrants (ascendantUuid, descendantUuid, managed, assumed, empowered) |
| | | values (userUuid, roleUuid, grantOptions.managed, grantOptions.assumed, grantOptions.empowered); |
| | | -- TODO: What should happen on mupltiple grants? What if options are not the same? |
| | | -- Most powerful or latest grant wins? What about managed? |
| | | -- on conflict do nothing; -- allow granting multiple times |
| | | end; $$; |
| | | --// |
| | | |
| | |
| | | return query select distinct perm.objectUuid |
| | | from (with recursive grants as (select descendantUuid, ascendantUuid, 1 as level |
| | | from RbacGrants |
| | | where follow |
| | | where assumed |
| | | and ascendantUuid = any (subjectIds) |
| | | union |
| | | distinct |
| | | select "grant".descendantUuid, "grant".ascendantUuid, level + 1 as level |
| | | from RbacGrants "grant" |
| | | inner join grants recur on recur.descendantUuid = "grant".ascendantUuid |
| | | where follow) |
| | | where assumed) |
| | | select descendantUuid |
| | | from grants) as granted |
| | | join RbacPermission perm |
| | |
| | | returns setof RbacPermission |
| | | strict |
| | | language sql as $$ |
| | | -- @formatter:off |
| | | -- @formatter:off |
| | | select * |
| | | from RbacPermission |
| | | where uuid in ( |
| | |
| | | |
| | | |
| | | -- ============================================================================ |
| | | --changeset rbac-views-GRANT-RESTRICTED-VIEW:1 endDelimiter:--// |
| | | -- ---------------------------------------------------------------------------- |
| | | /* |
| | | Creates a view to the grants table with row-level limitation |
| | | based on the direct grants of the current user. |
| | | */ |
| | | drop view if exists rbacgrants_rv; |
| | | create or replace view rbacgrants_rv as |
| | | select userName, objectTable||'#'||objectIdName||'.'||roletype as roleIdName, |
| | | managed, assumed, empowered, |
| | | ascendantUuid as userUuid, |
| | | descendantUuid as roleUuid, |
| | | objectTable, objectUuid, objectIdName, roleType |
| | | -- @formatter:off |
| | | from ( |
| | | select g.*, u.name as userName, o.objecttable, r.objectuuid, r.roletype, |
| | | findIdNameByObjectUuid(o.objectTable, o.uuid) as objectIdName |
| | | from rbacgrants as g |
| | | join rbacrole as r on r.uuid = g.descendantUuid |
| | | join rbacobject o on o.uuid = r.objectuuid |
| | | join rbacuser u on u.uuid = g.ascendantuuid |
| | | where isGranted(currentSubjectIds(), r.uuid) |
| | | ) as unordered |
| | | -- @formatter:on |
| | | order by objectIdName; |
| | | grant all privileges on rbacrole_rv to restricted; |
| | | --// |
| | | |
| | | |
| | | -- ============================================================================ |
| | | --changeset rbac-views-GRANTS-RV-INSERT-TRIGGER:1 endDelimiter:--// |
| | | -- ---------------------------------------------------------------------------- |
| | | |
| | | /** |
| | | Instead of insert trigger function for RbacGrants_RV. |
| | | */ |
| | | create or replace function insertRbacGrant() |
| | | returns trigger |
| | | language plpgsql as $$ |
| | | declare |
| | | newGrant RbacGrants_RV; |
| | | begin |
| | | if new.managed then |
| | | raise exception '[400] Managed grants cannot be inserted via RBacGrants_RV.'; |
| | | end if; |
| | | |
| | | call grantRoleToUser(new.roleUuid, new.userUuid, |
| | | ROW(false, new.assumed, new.empowered)); |
| | | select grv.* |
| | | from RbacGrants_RV grv |
| | | where grv.userUuid=new.userUuid and grv.roleUuid=new.roleUuid |
| | | into newGrant; |
| | | return newGrant; |
| | | end; $$; |
| | | |
| | | /* |
| | | Creates an instead of insert trigger for the RbacGrants_rv view. |
| | | */ |
| | | create trigger insertRbacGrant_Trigger |
| | | instead of insert |
| | | on RbacGrants_rv |
| | | for each row |
| | | execute function insertRbacGrant(); |
| | | |
| | | |
| | | -- ============================================================================ |
| | | --changeset rbac-views-USER-RESTRICTED-VIEW:1 endDelimiter:--// |
| | | -- ---------------------------------------------------------------------------- |
| | | /* |
| | |
| | | customerAdminUuid = createRole( |
| | | customerAdmin(NEW), |
| | | grantingPermissions(forObjectUuid => NEW.uuid, permitOps => array ['view', 'add-package']), |
| | | -- NO auto follow for customer owner to avoid exploding permissions for administrators |
| | | -- NO auto assume for customer owner to avoid exploding permissions for administrators |
| | | withUser(NEW.adminUserName, 'create') -- implicitly ignored if null |
| | | ); |
| | | |
New file |
| | |
| | | package net.hostsharing.hsadminng; |
| | | |
| | | import java.lang.annotation.ElementType; |
| | | import java.lang.annotation.Retention; |
| | | import java.lang.annotation.RetentionPolicy; |
| | | import java.lang.annotation.Target; |
| | | |
| | | @Retention(RetentionPolicy.RUNTIME) |
| | | @Target({ ElementType.TYPE, ElementType.METHOD }) |
| | | public @interface Accepts { |
| | | |
| | | String[] value(); |
| | | } |
| | |
| | | class FindAllByOptionalNameLike { |
| | | |
| | | @Test |
| | | public void hostsharingAdmin_withoutAssumedRole_canNotViewAnyPackages_becauseThoseGrantsAreNotFollowed() { |
| | | public void hostsharingAdmin_withoutAssumedRole_canNotViewAnyPackages_becauseThoseGrantsAreNotassumedd() { |
| | | // given |
| | | currentUser("mike@hostsharing.net"); |
| | | |
| | |
| | | } |
| | | |
| | | @Test |
| | | public void hostsharingAdmin_withAssumedHostsharingAdminRole__canNotViewAnyPackages_becauseThoseGrantsAreNotFollowed() { |
| | | public void hostsharingAdmin_withAssumedHostsharingAdminRole__canNotViewAnyPackages_becauseThoseGrantsAreNotassumedd() { |
| | | given: |
| | | currentUser("mike@hostsharing.net"); |
| | | assumedRoles("global#hostsharing.admin"); |
New file |
| | |
| | | package net.hostsharing.hsadminng.rbac.rbacgrant; |
| | | |
| | | import io.restassured.RestAssured; |
| | | import io.restassured.http.ContentType; |
| | | import net.hostsharing.hsadminng.Accepts; |
| | | import net.hostsharing.hsadminng.HsadminNgApplication; |
| | | import net.hostsharing.hsadminng.context.Context; |
| | | import net.hostsharing.hsadminng.rbac.rbacrole.RbacRoleEntity; |
| | | import net.hostsharing.hsadminng.rbac.rbacrole.RbacRoleRepository; |
| | | import net.hostsharing.hsadminng.rbac.rbacuser.RbacUserEntity; |
| | | import net.hostsharing.hsadminng.rbac.rbacuser.RbacUserRepository; |
| | | import net.hostsharing.test.JpaAttempt; |
| | | import org.apache.commons.lang3.RandomStringUtils; |
| | | import org.junit.jupiter.api.Test; |
| | | import org.springframework.beans.factory.annotation.Autowired; |
| | | import org.springframework.boot.test.context.SpringBootTest; |
| | | import org.springframework.boot.test.web.server.LocalServerPort; |
| | | |
| | | import javax.persistence.EntityManager; |
| | | import java.util.List; |
| | | import java.util.UUID; |
| | | |
| | | import static org.assertj.core.api.Assertions.assertThat; |
| | | import static org.hamcrest.Matchers.is; |
| | | |
| | | @SpringBootTest( |
| | | webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT, |
| | | classes = { HsadminNgApplication.class, JpaAttempt.class } |
| | | ) |
| | | @Accepts({ "ROL:S(Schema)" }) |
| | | class RbacGrantControllerAcceptanceTest { |
| | | |
| | | @LocalServerPort |
| | | Integer port; |
| | | |
| | | @Autowired |
| | | EntityManager em; |
| | | |
| | | @Autowired |
| | | Context context; |
| | | |
| | | @Autowired |
| | | RbacUserRepository rbacUserRepository; |
| | | |
| | | @Autowired |
| | | RbacRoleRepository rbacRoleRepository; |
| | | |
| | | @Autowired |
| | | RbacGrantRepository rbacGrantRepository; |
| | | |
| | | @Autowired |
| | | JpaAttempt jpaAttempt; |
| | | |
| | | @Test |
| | | @Accepts({ "ROL:L(List)" }) |
| | | void returnsRbacGrantsForPackageAdmin() { |
| | | |
| | | RestAssured // @formatter:off |
| | | .given() |
| | | .header("current-user", "aaa00@aaa.example.com") |
| | | .port(port) |
| | | .when() |
| | | .get("http://localhost/api/rbac-roles") |
| | | .then().assertThat() |
| | | .statusCode(200) |
| | | .contentType("application/json") |
| | | .body("[0].roleName", is("customer#aaa.tenant")) |
| | | .body("[1].roleName", is("package#aaa00.admin")) |
| | | .body("[2].roleName", is("package#aaa00.tenant")); |
| | | // @formatter:on |
| | | } |
| | | |
| | | @Test |
| | | @Accepts({ "ROL:C(Create)" }) |
| | | void packageAdmin_canGrantOwnPackageAdminRole_toArbitraryUser() { |
| | | |
| | | // given |
| | | final var givenNewUserName = "test-user-" + RandomStringUtils.randomAlphabetic(8) + "@example.com"; |
| | | final String givenPackageAdmin = "aaa00@aaa.example.com"; |
| | | final var givenOwnPackageAdminRole = "package#aaa00.admin"; |
| | | // when |
| | | RestAssured // @formatter:off |
| | | .given() |
| | | .header("current-user", givenPackageAdmin) |
| | | .contentType(ContentType.JSON) |
| | | .body(""" |
| | | { |
| | | "userUuid": "%s", |
| | | "roleUuid": "%s", |
| | | "assumed": true, |
| | | "empowered": false |
| | | } |
| | | """.formatted( |
| | | createRBacUser(givenNewUserName).getUuid().toString(), |
| | | findRbacRoleByName(givenOwnPackageAdminRole).getUuid().toString()) |
| | | ) |
| | | .port(port) |
| | | .when() |
| | | .post("http://localhost/api/rbac-grants") |
| | | .then().assertThat() |
| | | .statusCode(201); |
| | | // @formatter:on |
| | | |
| | | // then |
| | | assertThat(findAllGrantsOfUser(givenPackageAdmin)) |
| | | .extracting(RbacGrantEntity::toDisplay) |
| | | .contains("grant( " + givenNewUserName + " -> " + givenOwnPackageAdminRole + ": assumed )"); |
| | | } |
| | | |
| | | @Test |
| | | @Accepts({ "ROL:C(Create)", "ROL:X(Access Control)" }) |
| | | void packageAdmin_canNotGrantAlienPackageAdminRole_toArbitraryUser() { |
| | | |
| | | // given |
| | | final var givenNewUserName = "test-user-" + RandomStringUtils.randomAlphabetic(8) + "@example.com"; |
| | | final String givenPackageAdmin = "aaa00@aaa.example.com"; |
| | | final var givenAlienPackageAdminRole = "package#aab00.admin"; |
| | | |
| | | // when |
| | | RestAssured // @formatter:off |
| | | .given() |
| | | .header("current-user", givenPackageAdmin) |
| | | .contentType(ContentType.JSON) |
| | | .body(""" |
| | | { |
| | | "userUuid": "%s", |
| | | "roleUuid": "%s", |
| | | "assumed": true, |
| | | "empowered": false |
| | | } |
| | | """.formatted( |
| | | createRBacUser(givenNewUserName).getUuid().toString(), |
| | | findRbacRoleByName(givenAlienPackageAdminRole).getUuid().toString()) |
| | | ) |
| | | .port(port) |
| | | .when() |
| | | .post("http://localhost/api/rbac-grants") |
| | | .then().assertThat() |
| | | .statusCode(403); |
| | | // @formatter:on |
| | | |
| | | // then |
| | | assertThat(findAllGrantsOfUser(givenPackageAdmin)) |
| | | .extracting(RbacGrantEntity::getUserName) |
| | | .doesNotContain(givenNewUserName); |
| | | } |
| | | |
| | | List<RbacGrantEntity> findAllGrantsOfUser(final String userName) { |
| | | return jpaAttempt.transacted(() -> { |
| | | context.setCurrentUser(userName); |
| | | return rbacGrantRepository.findAll(); |
| | | }).returnedValue(); |
| | | } |
| | | |
| | | RbacUserEntity createRBacUser(final String userName) { |
| | | return jpaAttempt.transacted(() -> { |
| | | return rbacUserRepository.create(new RbacUserEntity(UUID.randomUUID(), userName)); |
| | | }).returnedValue(); |
| | | } |
| | | |
| | | RbacRoleEntity findRbacRoleByName(final String roleName) { |
| | | return jpaAttempt.transacted(() -> { |
| | | context.setCurrentUser("mike@hostsharing.net"); |
| | | return rbacRoleRepository.findByRoleName(roleName); |
| | | }).returnedValue(); |
| | | } |
| | | |
| | | } |
New file |
| | |
| | | package net.hostsharing.hsadminng.rbac.rbacgrant; |
| | | |
| | | import net.hostsharing.hsadminng.Accepts; |
| | | import net.hostsharing.hsadminng.context.Context; |
| | | import net.hostsharing.hsadminng.rbac.rbacrole.RbacRoleRepository; |
| | | import net.hostsharing.hsadminng.rbac.rbacuser.RbacUserRepository; |
| | | import org.junit.jupiter.api.Nested; |
| | | import org.junit.jupiter.api.Test; |
| | | import org.springframework.beans.factory.annotation.Autowired; |
| | | import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest; |
| | | import org.springframework.context.annotation.ComponentScan; |
| | | import org.springframework.orm.jpa.JpaSystemException; |
| | | import org.springframework.test.annotation.DirtiesContext; |
| | | |
| | | import javax.persistence.EntityManager; |
| | | import java.util.List; |
| | | |
| | | import static net.hostsharing.test.JpaAttempt.attempt; |
| | | import static org.assertj.core.api.Assertions.assertThat; |
| | | |
| | | @DataJpaTest |
| | | @ComponentScan(basePackageClasses = { Context.class, RbacGrantRepository.class }) |
| | | @DirtiesContext |
| | | @Accepts({ "GRT:S(Schema)" }) |
| | | class RbacGrantRepositoryIntegrationTest { |
| | | |
| | | @Autowired |
| | | Context context; |
| | | |
| | | @Autowired |
| | | RbacGrantRepository rbacGrantRepository; |
| | | |
| | | @Autowired |
| | | RbacUserRepository rbacUserRepository; |
| | | |
| | | @Autowired |
| | | RbacRoleRepository rbacRoleRepository; |
| | | |
| | | @Autowired |
| | | EntityManager em; |
| | | |
| | | @Nested |
| | | class FindAllRbacGrants { |
| | | |
| | | @Test |
| | | @Accepts({ "GRT:L(List)" }) |
| | | public void packageAdmin_canViewItsRbacGrants() { |
| | | // given |
| | | currentUser("aaa00@aaa.example.com"); |
| | | |
| | | // when |
| | | final var result = rbacGrantRepository.findAll(); |
| | | |
| | | // then |
| | | exactlyTheseRbacGrantsAreReturned( |
| | | result, |
| | | "grant( aaa00@aaa.example.com -> package#aaa00.admin: managed assumed empowered )"); |
| | | } |
| | | |
| | | @Test |
| | | @Accepts({ "GRT:L(List)" }) |
| | | public void customerAdmin_canViewItsRbacGrants() { |
| | | // given |
| | | currentUser("admin@aaa.example.com"); |
| | | |
| | | // when |
| | | final var result = rbacGrantRepository.findAll(); |
| | | |
| | | // then |
| | | exactlyTheseRbacGrantsAreReturned( |
| | | result, |
| | | "grant( admin@aaa.example.com -> customer#aaa.admin: managed assumed empowered )", |
| | | "grant( aaa00@aaa.example.com -> package#aaa00.admin: managed assumed empowered )", |
| | | "grant( aaa01@aaa.example.com -> package#aaa01.admin: managed assumed empowered )", |
| | | "grant( aaa02@aaa.example.com -> package#aaa02.admin: managed assumed empowered )"); |
| | | } |
| | | |
| | | @Test |
| | | @Accepts({ "GRT:L(List)" }) |
| | | public void customerAdmin_withAssumedRole_cannotViewRbacGrants() { |
| | | // given: |
| | | currentUser("admin@aaa.example.com"); |
| | | assumedRoles("package#aab00.admin"); |
| | | |
| | | // when |
| | | final var result = attempt( |
| | | em, |
| | | () -> rbacGrantRepository.findAll()); |
| | | |
| | | // then |
| | | result.assertExceptionWithRootCauseMessage( |
| | | JpaSystemException.class, |
| | | "[403] user admin@aaa.example.com", "has no permission to assume role package#aab00#admin"); |
| | | } |
| | | } |
| | | |
| | | @Nested |
| | | class CreateRbacGrant { |
| | | |
| | | @Test |
| | | @Accepts({ "GRT:C(Create)" }) |
| | | public void customerAdmin_canGrantOwnPackageAdminRole_toArbitraryUser() { |
| | | // given |
| | | currentUser("admin@aaa.example.com"); |
| | | final var userUuid = rbacUserRepository.findUuidByName("aac00@aac.example.com"); |
| | | final var roleUuid = rbacRoleRepository.findByRoleName("package#aaa00.admin").getUuid(); |
| | | |
| | | // when |
| | | final var grant = RbacGrantEntity.builder() |
| | | .userUuid(userUuid).roleUuid(roleUuid) |
| | | .assumed(true).empowered(false) |
| | | .build(); |
| | | final var attempt = attempt(em, () -> |
| | | rbacGrantRepository.save(grant) |
| | | ); |
| | | |
| | | // then |
| | | assertThat(attempt.wasSuccessful()).isTrue(); |
| | | assertThat(rbacGrantRepository.findAll()) |
| | | .extracting(RbacGrantEntity::toDisplay) |
| | | .contains("grant( aac00@aac.example.com -> package#aaa00.admin: assumed )"); |
| | | } |
| | | } |
| | | |
| | | void currentUser(final String currentUser) { |
| | | context.setCurrentUser(currentUser); |
| | | assertThat(context.getCurrentUser()).as("precondition").isEqualTo(currentUser); |
| | | } |
| | | |
| | | void assumedRoles(final String assumedRoles) { |
| | | context.assumeRoles(assumedRoles); |
| | | assertThat(context.getAssumedRoles()).as("precondition").containsExactly(assumedRoles.split(";")); |
| | | } |
| | | |
| | | void exactlyTheseRbacGrantsAreReturned(final List<RbacGrantEntity> actualResult, final String... expectedGrant) { |
| | | assertThat(actualResult) |
| | | .filteredOn(g -> !g.getUserName().startsWith("test-user-")) // ignore test-users created by other tests |
| | | .extracting(RbacGrantEntity::toDisplay) |
| | | .containsExactlyInAnyOrder(expectedGrant); |
| | | } |
| | | |
| | | } |
New file |
| | |
| | | package net.hostsharing.hsadminng.rbac.rbacrole; |
| | | |
| | | import io.restassured.RestAssured; |
| | | import net.hostsharing.hsadminng.Accepts; |
| | | import net.hostsharing.hsadminng.HsadminNgApplication; |
| | | import net.hostsharing.hsadminng.context.Context; |
| | | import net.hostsharing.hsadminng.rbac.rbacuser.RbacUserRepository; |
| | | import org.junit.jupiter.api.Test; |
| | | import org.springframework.beans.factory.annotation.Autowired; |
| | | import org.springframework.boot.test.context.SpringBootTest; |
| | | import org.springframework.boot.test.web.server.LocalServerPort; |
| | | |
| | | import javax.persistence.EntityManager; |
| | | |
| | | import static org.hamcrest.Matchers.is; |
| | | |
| | | @SpringBootTest( |
| | | webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT, |
| | | classes = HsadminNgApplication.class |
| | | ) |
| | | @Accepts({ "ROL:*:S:Schema" }) |
| | | class RbacRoleControllerAcceptanceTest { |
| | | |
| | | @LocalServerPort |
| | | private Integer port; |
| | | |
| | | @Autowired |
| | | EntityManager em; |
| | | |
| | | @Autowired |
| | | Context context; |
| | | |
| | | @Autowired |
| | | RbacUserRepository rbacUserRepository; |
| | | |
| | | @Autowired |
| | | RbacRoleRepository rbacRoleRepository; |
| | | |
| | | @Test |
| | | @Accepts({ "ROL:*:L:List" }) |
| | | void returnsRbacRolesForAssumedPackageAdmin() { |
| | | |
| | | // @formatter:off |
| | | RestAssured |
| | | .given() |
| | | .header("current-user", "mike@hostsharing.net") |
| | | .header("assumed-roles", "package#aaa00.admin") |
| | | .port(port) |
| | | .when() |
| | | .get("http://localhost/api/rbac-roles") |
| | | .then().assertThat() |
| | | .statusCode(200) |
| | | .contentType("application/json") |
| | | .body("[0].roleName", is("customer#aaa.tenant")) |
| | | .body("[1].roleName", is("package#aaa00.admin")) |
| | | .body("[2].roleName", is("package#aaa00.tenant")); |
| | | // @formatter:on |
| | | } |
| | | |
| | | } |
| | |
| | | import java.util.UUID; |
| | | |
| | | import static org.assertj.core.api.Assertions.assertThat; |
| | | import static org.hamcrest.Matchers.is; |
| | | import static org.hamcrest.Matchers.startsWith; |
| | | import static org.hamcrest.Matchers.*; |
| | | |
| | | @SpringBootTest( |
| | | webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT, |
| | |
| | | .body("[0].name", is("aaa00@aaa.example.com")) |
| | | .body("[1].name", is("aaa01@aaa.example.com")) |
| | | .body("[2].name", is("aaa02@aaa.example.com")) |
| | | .body("size()", is(14)); |
| | | .body("[3].name", is("aab00@aab.example.com")) |
| | | // ... |
| | | .body("[11].name", is("admin@aac.example.com")) |
| | | .body("[12].name", is("mike@hostsharing.net")) |
| | | .body("[13].name", is("sven@hostsharing.net")) |
| | | .body("size()", greaterThanOrEqualTo(14)); |
| | | // @formatter:on |
| | | } |
| | | |
| | |
| | | |
| | | void exactlyTheseRbacUsersAreReturned(final List<RbacUserEntity> actualResult, final String... expectedUserNames) { |
| | | assertThat(actualResult) |
| | | .filteredOn(u -> !u.getName().startsWith("test-user-")) |
| | | .extracting(RbacUserEntity::getName) |
| | | .containsExactlyInAnyOrder(expectedUserNames); |
| | | } |
| | |
| | | } |
| | | } |
| | | |
| | | public static JpaResult<Void> attempt(final EntityManager em, final Runnable code) { |
| | | return attempt(em, () -> { |
| | | code.run(); |
| | | return null; |
| | | }); |
| | | } |
| | | |
| | | @Transactional(propagation = Propagation.REQUIRES_NEW) |
| | | public <T> JpaResult<T> transacted(final Supplier<T> code) { |
| | | return attempt(em, code); |